Skip to content

feat(slides): add Presentation.sections — slide CRUD Phase 4 (closes #11)#36

Merged
MHoroszowski merged 1 commit into
masterfrom
feature/slide-crud-phase4
May 8, 2026
Merged

feat(slides): add Presentation.sections — slide CRUD Phase 4 (closes #11)#36
MHoroszowski merged 1 commit into
masterfrom
feature/slide-crud-phase4

Conversation

@MHoroszowski
Copy link
Copy Markdown
Owner

Slide CRUD Phase 4 — Presentation.sections

Closes #11. Phase 4 of the slide CRUD epic. With this PR the seven sub-features in #11 are all shipped — Phase 1 (#33), Phase 2 (#34), Phase 3 (#35), and now Phase 4 here.

What this adds

Presentation.sections — a sequence-like collection over the p:extLst/p:ext{uri=521415D9-…}/p14:sectionLst extension that PowerPoint uses to organize slides into named groups in the slide pane.

prs = Presentation("deck.pptx")

# add a section
intro = prs.sections.add_section("Intro")

# move slides into it
intro.add_slide(prs.slides[0])
intro.add_slide(prs.slides[1])

# rename
intro.name = "Cover"

# read membership
[s.slide_id for s in prs.sections[0].slides]

# drop a section (slides remain in prs)
prs.sections.remove(intro)

Public API

  • Presentation.sections_Sections
  • _Sections: __len__, __iter__, __getitem__, index(section), add_section(name, after=None), remove(section)
  • Section: name (read/write), id (read-only GUID), slides (tuple), add_slide(slide), remove_slide(slide)

Membership invariants

Section membership references slides by their stable p:sldId/@id integer (NOT by r:id and NOT by position), so:

  • Slides.move(...) — section assignment survives reorder ✓
  • Slides.remove(...) — surviving slides keep their assignment ✓
  • Slides.add_slide(layout, index=N) — new slides are unsectioned, existing assignments untouched ✓
  • A slide can belong to at most one section. Section.add_slide(slide) automatically removes it from any other section first.

When a slide is removed from the presentation, its slide_id stays as an orphan in the section's p14:sldIdLst. Section.slides silently skips orphans on read; the XML round-trips them untouched. This deliberately matches python-pptx's "preserve foreign data" doctrine — we don't auto-prune.

PowerPoint compatibility

  • Empty sections emit <p14:sldIdLst/> so PowerPoint versions that interpret an omitted sldIdLst as "all unsectioned slides" don't pull surprises.
  • Section ids generated as {<UPPER-CASE-GUID>} matching PowerPoint's wire shape — e.g. {4080CFE4-F95C-449C-8898-95C81DD3D8B4}.
  • Foreign p:extLst/p:ext siblings (p15:* modification tracking, etc.) round-trip untouched — the section ext lives alongside, not in place of.

Internal additions

  • New pptx/sections.py module hosting Section and _Sections proxy classes.
  • pptx/oxml/presentation.py extended with CT_PresentationExtensionList, CT_PresentationExtension, CT_SectionList, CT_Section, CT_SectionSlideIdList, CT_SectionSlideId, plus SECTION_LIST_EXT_URI constant and CT_Presentation.{section_list, get_or_add_section_list, remove_section_list} helpers.
  • pptx/oxml/ns.py registers the p14 prefix (http://schemas.microsoft.com/office/powerpoint/2010/main).
  • pptx/oxml/__init__.py registers the new element classes.
  • p:extLst added as a successor entry on the existing ZeroOrOne sldMasterIdLst / sldIdLst / sldSz slots so insert ordering stays correct.

Tangential test-infra fix

tests/unitutil/cxml.py namespace-prefix grammar widened from Word(alphas) to Word(alphas, alphanums) so test fixtures can address p14:, w14:, o15:, etc. Local-name grammar already supported alphanums.

Test coverage

  • 56 new unit tests in tests/test_sections.py:
    • oxml helpers (CT_SectionList, CT_SectionSlideIdList, the CT_Presentation section-list traversal helpers)
    • the Section / _Sections proxies
    • 8 round-trip integration tests including: no-sections baseline, names + membership, GUID preservation, membership-survives-move, removal pruning, orphan preservation through Slides.remove, empty-section round-trip, unicode + XML-special chars in section name, unsectioned-on-section-remove, sibling-ext preservation.
  • 5 new behave scenarios in features/sld-sections.feature (default empty, add, slide membership, move-preserves-membership, remove cleans up extLst).
  • UAT script uat_slide_sections.py (untracked per repo §6) builds a 6-slide / 3-section deck, demonstrates membership preservation through a slide move, and prints structural read-back. UAT signoff: ✓.

Out of scope (follow-up issues)

These were considered and deliberately left for separate PRs to keep this one reviewable:

  • append_from cross-deck section porting: when source has sections and is append_from'd into a target, sections are not currently ported. The append_from machinery is Phase 3 territory; this is a worthy follow-up.
  • PowerPoint-authored fixture round-trip: the round-trip tests use synthetic decks. A real PowerPoint-authored sectioned deck reopened and saved unchanged (XML-canonical-diff) would be the highest-confidence regression guard. Belongs in a manual UAT matrix, not unit tests.
  • Per-slide / per-shape p:custDataLst tags: orthogonal to sections (different extension), tracked separately.

Verification

$ python3 -m pytest tests/ -q | tail -3
3222 passed in 4.33s

$ ruff check src tests | tail -3
All checks passed!

$ python3 -m behave features/ --no-color | tail -3
999 scenarios passed, 0 failed, 0 skipped
3000 steps passed, 0 failed, 0 skipped

)

Phase 4 of issue #11 (slide CRUD epic). Implements PowerPoint Sections
support — read, write, and round-trip the
`p:extLst/p:ext{uri=521415D9-…}/p14:sectionLst` extension that
PowerPoint uses to organize slides into named groups in the slide
pane. The fork's slide CRUD epic is now complete: Phase 1 (#33),
Phase 2 (#34), Phase 3 (#35), and Phase 4 (this PR) collectively
shipped duplicate / delete / reorder / copy-between-decks / sections
across the originally listed sub-features.

New public API
--------------
- `Presentation.sections` → `_Sections` collection.
- `_Sections` is sequence-like: `__len__`, `__iter__`,
  `__getitem__`, `index()`. Adds:
  * `add_section(name, after=None) -> Section` — append, or insert
    immediately after an existing section when `after` is given.
  * `remove(section)` — drop section; cleans up the wrapping
    `p14:sectionLst` / `p:ext` / `p:extLst` chain when the last
    section goes.
- `Section` exposes:
  * `name` (read/write str)
  * `id` (read-only GUID-with-braces, e.g. `{ABC-…}`)
  * `slides` (tuple of |Slide|, in section order)
  * `add_slide(slide)` — assign or move slide into this section
    (a slide can belong to at most one section)
  * `remove_slide(slide)` — drop a slide's section assignment;
    slide remains in the presentation.

Membership invariants
---------------------
Section membership references slides by their stable
`p:sldId/@id` integer (NOT by `r:id` and NOT by position), so
`Slides.move(...)`, indexed `add_slide(...)`, and `Slides.remove(...)`
preserve assignment without any extra plumbing. Removed slides
become orphan ids in the section's `p14:sldIdLst`; `Section.slides`
silently skips them on read but the XML round-trips them untouched
(deliberate — matching python-pptx's "preserve foreign data"
doctrine).

PowerPoint compatibility
------------------------
- Empty sections emit `<p14:sldIdLst/>` so all PowerPoint versions
  treat them consistently (some interpret an omitted `sldIdLst`
  as "all unsectioned slides," which is not what we mean).
- Section ids generated as `{<UPPER-CASE-GUID>}` matching
  PowerPoint's wire shape.
- Foreign `p:extLst/p:ext` siblings (`p15:*`, modification
  tracking, etc.) round-trip untouched — the section ext lives
  alongside, not in place of.

Internal additions
------------------
- New `pptx/sections.py` module hosting `Section` and
  `_Sections` proxy classes.
- `pptx/oxml/presentation.py` extended with
  `CT_PresentationExtensionList`, `CT_PresentationExtension`,
  `CT_SectionList`, `CT_Section`, `CT_SectionSlideIdList`,
  `CT_SectionSlideId`, plus `SECTION_LIST_EXT_URI` constant and
  `CT_Presentation.{section_list, get_or_add_section_list,
  remove_section_list}` helpers.
- `pptx/oxml/ns.py` registers the `p14` prefix
  (`http://schemas.microsoft.com/office/powerpoint/2010/main`).
- `pptx/oxml/__init__.py` registers the new element classes.
- `p:extLst` added as a successor entry on the existing
  ZeroOrOne `sldMasterIdLst`/`sldIdLst`/`sldSz` slots so
  insert ordering is correct.

Tangential test-infra fix
-------------------------
`tests/unitutil/cxml.py` namespace-prefix grammar widened from
`Word(alphas)` to `Word(alphas, alphanums)` so test fixtures
can address `p14:`, `w14:`, `o15:`, etc. Local-name grammar
already supported alphanums.

Test coverage
-------------
- 56 new unit tests in `tests/test_sections.py` covering oxml
  helpers, the `Section`/`_Sections` proxies, and 8 round-trip
  integration tests (no-sections baseline, names + membership,
  GUID preservation, membership-survives-move, removal pruning,
  orphan preservation, empty-section, unicode/XML-special name,
  unsectioned-on-section-remove, sibling-ext preservation).
- 5 new behave scenarios in `features/sld-sections.feature`
  (default empty, add, slide membership, move-preserves-membership,
  remove cleans up extLst).
- New `uat_slide_sections.py` (untracked per repo §6) builds a
  6-slide / 3-section deck, demonstrates membership preservation
  through a slide move, and prints a structural read-back.

Verification
------------
```
$ python3 -m pytest tests/ -q | tail -3
3222 passed in 4.33s

$ ruff check src tests | tail -3
All checks passed!

$ python3 -m behave features/ --no-color | tail -3
999 scenarios passed, 0 failed, 0 skipped
3000 steps passed, 0 failed, 0 skipped
```

Closes #11
@MHoroszowski MHoroszowski merged commit 76291a2 into master May 8, 2026
12 checks passed
@MHoroszowski MHoroszowski deleted the feature/slide-crud-phase4 branch May 8, 2026 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Epic] Slide CRUD & Sections — duplicate, delete, reorder, copy-between-decks

1 participant